package nl.utwente.ewi.hmi.multitouch.drivers.diamondtouch;

import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import nl.utwente.ewi.hmi.multitouch.SimpleTangible;
import nl.utwente.ewi.hmi.multitouch.SurfaceType;
import nl.utwente.ewi.hmi.multitouch.Tangible;
import nl.utwente.ewi.hmi.multitouch.analysis.Segment;
import nl.utwente.ewi.hmi.multitouch.drivers.dtproxy.DiamondTouchSegment;
import nl.utwente.ewi.hmi.multitouch.io.TouchStream;

import com.merl.diamondtouch.DtlibDevice;
import com.merl.diamondtouch.DtlibException;
import com.merl.diamondtouch.DtlibInputTframe;
import com.merl.diamondtouch.DtlibSegment;

/**
 * The DiamondTouchStream is a TouchStream implementation of the DiamondTouch jdt library.
 * 
 * @author Michiel Hakvoort
 * @version 1.0
 *
 */
public class DiamondTouchStream extends TouchStream {

	private final DtlibDevice device;

	private final Tangible[] tangibles;

	private final int width;
	private final int userCount;
	
	private final int[] xSegments;
	private final int[] ySegments;
	private final DtlibSegment[][] cachedXSegments;
	private final DtlibSegment[][] cachedYSegments;

	private final int[] holdFrame;
	
	private final Set<Object> owners;
	
	/**
	 * Create a new DiamondTouchStream for the given {@linkplain DtlibDevice} and {@linkplain DiamondTouchDeviceInfo}.
	 * 
	 * @param device The {@linkplain DtlibDevice} to read frames from
	 * @param touchDeviceInfo The {@linkplain DiamondTouchDeviceInfo} used to determine the maximum amount of concurrent users.
	 */
	public DiamondTouchStream(DtlibDevice device, DiamondTouchDeviceInfo touchDeviceInfo) {
		super(touchDeviceInfo);
		this.device = device;
		this.owners = new HashSet<Object>();
		

		this.width = touchDeviceInfo.getSize().width;
		this.userCount = touchDeviceInfo.getUserCount();
		
		this.xSegments = new int[userCount];
		this.ySegments = new int[userCount];
		this.cachedXSegments = new DtlibSegment[userCount][];
		this.cachedYSegments = new DtlibSegment[userCount][];

		this.holdFrame = new int[userCount];
		
		this.tangibles = new Tangible[userCount];

		for(int i = 0; i < userCount; i++) {
			final int c = i;
			final Object owner = new Object() {
				@Override
				public String toString() {
					return "DiamondTouch user " + c;
				}
			};
			this.owners.add(owner);
			
			this.tangibles[i] = new SimpleTangible(owner, SurfaceType.UNKNOWN);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized boolean close() throws IOException {
		if(device.getStatus() != DtlibDevice.STATUS_STARTED) {
			return false;
		}
		
		device.stop();
		return true;
	}

	@Override
	public synchronized void read(Set<Segment> segments) throws IOException {
		final DtlibInputTframe[] frames;

		try {
			frames = (DtlibInputTframe[]) device.read();
		} catch(DtlibException e) {
			throw new IOException(e);
		}
		
		if(frames == null) {
			return;
		}

		final long time = System.currentTimeMillis();
		
		for(int i = 0; i < frames.length; i++) {
			final DtlibInputTframe frame = frames[i];

//			final long time = frame.getTimestampMs();
			final Tangible tangible = this.tangibles[i];
			
			if(frame == null) {
				System.out.println("meow");
				continue;
			}

			DtlibSegment[] xSegments = frame.getXSegments();
			DtlibSegment[] ySegments = frame.getYSegments();

			final int previousXSegmentCount = this.xSegments[i];
			final int previousYSegmentCount = this.ySegments[i];

			this.xSegments[i] = xSegments.length;
			this.ySegments[i] = ySegments.length;

			boolean useCache = false;

			// If only the amount of horizontal frames was increased by one or
			// only the amount of vertical frames was increased by one, wait
			// for at most one frame... then continue operation.
			if(holdFrame[i] > 0) {
				holdFrame[i]--;
				useCache = holdFrame[i] > 0;
			} else if((xSegments.length > previousXSegmentCount) ^ (ySegments.length > previousYSegmentCount)) {
				this.holdFrame[i] = 4;
				useCache = true;
			} else if((xSegments.length < previousXSegmentCount) ^ (ySegments.length < previousYSegmentCount)) {
				this.holdFrame[i] = 4;
				useCache = true;
			}
			
			if(useCache && this.cachedXSegments[i] != null) {
				xSegments = this.cachedXSegments[i];
				ySegments = this.cachedYSegments[i];
			} else {
				this.cachedXSegments[i] = xSegments;
				this.cachedYSegments[i] = ySegments;
			}

			for(DtlibSegment xSegment : xSegments) {
				for(DtlibSegment ySegment : ySegments) {
					segments.add(new DTSegment(xSegment, ySegment, time, tangible));
				}
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized boolean isOpen() {
		return device.getStatus() == DtlibDevice.STATUS_STARTED;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized boolean open() throws IOException {
		if(device.getStatus() == DtlibDevice.STATUS_STARTED) {
			return false;
		}

		if(device.start(DtlibInputTframe.getClassInputclass()) != 0) {
			throw new IOException("Could not start DiamondTouch device");
		}

		return true;
	}

	@Override
	public Set<Object> getActors() {
		return Collections.unmodifiableSet(this.owners);
	}

	private static final class DTSegment implements Segment {

		final Rectangle2D bounds;
		final float mass;
		final Point2D origin;
		final Path2D perimeter;
		final Tangible tangible;
		final long time;

		public DTSegment(DtlibSegment xSegment, DtlibSegment ySegment, long time, Tangible tangible) {
			int width = xSegment.stopPos - xSegment.startPos;
			int height = ySegment.stopPos - ySegment.startPos;

			this.bounds = new Rectangle2D.Float(xSegment.startPos, ySegment.startPos, width, height);
			this.perimeter = new Path2D.Float(this.bounds);
			this.origin = new Point2D.Float((float)this.bounds.getCenterX(), (float)this.bounds.getCenterY());

			this.time = time;
			this.tangible = tangible;
			this.mass = width * height;
		}

		@Override
		public Rectangle2D getBounds() {
			return this.bounds;
		}

		@Override
		public float getMass() {
			return this.mass;
		}

		@Override
		public Point2D getOrigin() {
			return this.origin;
		}

		@Override
		public Path2D getPerimeter() {
			return this.perimeter;
		}

		@Override
		public Tangible getTangible() {
			return this.tangible;
		}

		@Override
		public long getTime() {
			return this.time;
		}

		@Override
		public boolean isAmbiguous() {
			return true;
		}

		@Override
		public int compareTo(Segment o) {
			return (int) (this.getTime() - o.getTime());
		}
		
	}
}
